Parse, don’t validate
Alexis King氏のエッセイ
Parse, don’t validate
邦訳
1つのエッセイの中で、いくつかのことを主張している
このノートではエッセイの流れや要点のみを書くmrsekut.icon
主張は分散させて別のノートに書く
要するに、
プログラムの外部との境界で、正しい仕様を表現したデータ構造に変換すると、色々嬉しい
失敗しうるデータ構造を内部にまで入れない
境界部分で全て根絶させる
流れとしてはこんな感じ
序盤で入力の型を厳格にすることでhandlingを避けるについて書いている
プログラムの外部から内部に入る時に失敗し得ない型にすると良い
ここで言う失敗しうる型はMaybe aで、失敗し得ない型はNonEmpty a
失敗し得ない型にすることで、内部ではhandlingが不要になり、実装がシンプルになる
中盤は、validationではなく、parseしようという話
値の正当性を確かめる時に、validationではなくparseしよう
ただ値チェックするだけのvalidationは漏れうるし、handlingも増える
parseし、仕様を満たしたデータ構造に変換すると良い
内部がシンプルになる
静的に漏れていないことを確認できる
後半は、Parse, don't validateの実践例
元々2つの課題があった
プログラム中でnull checkのようなhandlingが頻発するとダルい
仕様を満たしていることをチェックすることを強制したい
後者をやるためにはparseするしかない
前者に対しては、validation、parseであまり差がないと思うmrsekut.icon
どちらも境界部分でやればいい
このエッセイはなぜかvalidationを境界でやることをあまり前提していない
だからプログラム中でvalidationが必要になってshotgun parsingが出てきてだるいよね、みたいな話にもなっている
用語の注意
validate
値が正しいかどうかをチェックする
正しい場合は、voidを返す
このエッセイでのvalidateはvoidを返すmrsekut.icon
実際は、Boolを返すようなものもvalidateに含まれると思うmrsekut.icon
正しくない場合はthrowする
code:例.hs
validateNonEmpty :: a -> IO ()
validateNonEmpty (_:_) = pure ()
validateNonEmpty [] = throwIO $ userError "list cannot be empty"
parse
構造的でない外部の入力を、構造的なデータに変換する
正しい場合は、それを型に込めて値を返す
正しくない場合はthrowする
a parser is just a function that consumes less-structured input and produces more-structured output. ref
code:例.hs
parseNonEmpty :: a -> IO (NonEmpty a)
parseNonEmpty (x:xs) = pure (x:|xs)
parseNonEmpty [] = throwIO $ userError "list cannot be empty"
本来は、両者ともthrowを返す必要はないが、恐らく説明のわかりやすさのためにthrowしているmrsekut.icon
Maybeなどを使っても良いはずだが、そうするとvalidateの説明がわかりづらくなる
validateには問題がある
チェック自体を強制できない
漏れる
チェックする関数を呼び忘れたことに気づけない
境界に置くことを強制できない
validate関数は、返り値の型がVoidなので、どこで実行するかに制限を設けられない
だから、プログラムの内部もで自由に実行できてしまう
すると、handlingがプログラム内に分散する
チェック済みであるという情報を伝達できない
[(key,value)]というデータが渡ってきた時に、「keyに重複がない」ことをチェック済みかどうか判別できない
parseの結果を信頼できるものにする
こうではなく
code:js
const data = parse(input); // dataはまだ信頼できない
if (validate(data)) {
const trusted = data;
}
こういうものをshotgun parsingと呼ぶ
これはアンチパターン
こうする
code:ts
try {
const trusted = parse(input); // 結果は信頼できる
} catch (error) {
throw new Error();
}
parseの結果が100%信頼できるようにする
parseを全域関数として定義する
関連
『Ghosts of Departed Proofs』
#WIP
https://slides.com/anthony-1/deck-1c0c6a
speaker notesがちゃんとあるmrsekut.icon
前半の話の例 ref
code:hs
getConfigurationDirectories :: IO FilePath
getConfigurationDirectories = do
configDirsString <- getEnv "CONFIG_DIRS"
let configDirsList = split ',' configDirsString
when (null configDirsList) $
throwIO $ userError "CONFIG_DIRS cannot be empty"
pure configDirsList
getConfigurationDirectoriesは、[FilePath]を返す
この関数内ではenvから文字列を読み込んで、空でないかチェックした後に、[FilePath]を返している
うーん、これそんなに問題になるか #??
この関数でcheckがあろうとなかろうと、[FilePath]は「正しいpathのリスト」という扱いにすれば問題なくないか?
これが空リストになるかどうかはあまり関係なくない?mrsekut.icon
実際には、このコード例にはmainもあるけどこれはセットで見ないといけないか
mainの中ではheadを使っており、Nothingの方は現時点では到達することがない
それは、getConfigurationDirectories内でチェックしているから。
このチェックがなくなった場合、error "should never..."のところに到達してしまう
これは最初想定していなかったerrorがthrowされることになるので問題
という感じだろうかmrsekut.icon
code:hs
main :: IO ()
main = do
configDirs <- getConfigurationDirectories
case head configDirs of
Just cacheDir -> initializeCache cacheDir
Nothing -> error "should never happen; already checked configDirs is non-empty"